/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

 ****/
package org.activequant.calendar;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;

import org.activequant.core.types.Exchange;
import org.activequant.core.types.TimeStamp;
import org.activequant.core.util.Check;
import org.activequant.math.algorithms.Interval;
import org.activequant.util.calendar.CalendarUtils;
import org.apache.log4j.Logger;

/**
 * A calendar for an exchange.<br/>
 * Note that an exchange has a time zone, all interaction regarding times and dates with the exchange are in GMT time,
 * as most of the interactions will be with <code>TimeStamp</code> objects.<br>
 * This class is immutable. <br/>
 * ExchangeCalendar implements IExchangeCalendar. Holds the following associated variables:
 * <ul>
 * <li>exchange(Exchange)</li>
 * <li>timeZone(TimeZone)</li>
 * <li>openingMillis(long)</li>
 * <li>closingMillis(long)</li>
 * <li>holidays(IHolidayCalendar)</li>
 * <li>entries(List&lt;ICalendarEntry&gt;)</li>
 * </ul>
 * <b>History:</b><br>
 * - [May 31, 2008] Created (mark) <br>
 * - [Aug 17, 2008] made it immutable<br>
 * 
 * @author mark
 */
public class ExchangeCalendar implements IExchangeCalendar {
	private static final long serialVersionUID = 2044772577431040848L;
	@SuppressWarnings("unused")
	private static final Logger logger = Logger.getLogger(ExchangeCalendar.class.getName());
	/**
	 * private final Exchange exchange;
	 */
	private final Exchange exchange;
	/**
	 * private final TimeZone timeZone;
	 */
	private final TimeZone timeZone;
	/**
	 * private final long openingMillis;<br/>
	 * in milliseconds after midnight in the exchange's time zone
	 */
	private final long openingMillis;
	/**
	 * private final long closingMillis;<br/>
	 * in milliseconds after midnight in the exchange's time zone
	 */
	private final long closingMillis;
	/**
	 * private IHolidayCalendar holidays;
	 */
	private IHolidayCalendar holidays;
	/**
	 * private final List&lt;ICalendarEntry&gt; entries = new ArrayList&lt;ICalendarEntry&gt;();
	 */
	private final List<ICalendarEntry> entries = new ArrayList<ICalendarEntry>();
	/**
	 * constructs an ExchangeCalendar (implements IExchangeCalendar) using the given settings(IExchangeCalendarSettings) to set its associated
	 * exchange(Exchange), timeZone(TimeZone), openingMillis(long) and closingMillis(long).
	 * @param settings
	 */
	private ExchangeCalendar(final IExchangeCalendarSettings settings) {
		Check.notNull("settings", settings);

		this.exchange = settings.getExchange();
		this.timeZone = settings.getTimeZone();
		this.openingMillis = settings.getRegularOpeningTime();
		this.closingMillis = settings.getRegularClosingTime();
	}

	/**
	 * Create a new exchange calendar given the exchange calendar settings and holiday calendar.<br/>
	 * constructs an ExchangeCalendar (implements IExchangeCalendar) using the given settings(IExchangeCalendarSettings) to set its associated
	 * exchange(Exchange), timeZone(TimeZone), openingMillis(long) and closingMillis(long).<br/>
	 * Sets its associated holidays(IHolidayCalendar) with the given holidays(IHolidayCalendar). The timeZone(TimeZone) in the given holidays(IHolidayCalendar) must match the one
	 * in the given settings(IExchangeCalendarSettings).
	 * @param settings
	 *            the exchange calendar settings for this exchange calendar
	 * @param holidays
	 *            the holiday calendar for the exchange calendar
	 */
	public ExchangeCalendar(final IExchangeCalendarSettings settings, final IHolidayCalendar holidays) {
		this(settings);
		Check.notNull("holidays", holidays);
		Check.notNullAndEqual("exchange calendar timezone", "holidays timezone", this.timeZone, holidays.getTimeZone());
		this.holidays = holidays;
	}
	/**
	 * returns the associated openingMillis(long)
	 */
	public long getRegularOpeningTime() {
		return this.openingMillis;
	}
	/**
	 * returns the associated closingMillis(long)
	 */
	public long getRegularClosingTime() {
		return this.closingMillis;
	}
	/**
	 * Returns the next opening time of the exchange after the given after(TimeStamp), taking into account holidays and business days defined for the exchange by this ExchangeCalendar.
	 */
	public TimeStamp getNextOpeningAsTimeStamp(final TimeStamp after) {
		Check.notNull("after", after);

		TimeStamp nextOpening = after;
		if (todaysOpening(nextOpening) == null || after.isAfter(todaysOpening(nextOpening))) {
			/*
			 * We do not open today, or 'after' is after the opening bell. Skip today...
			 */
			nextOpening = CalendarUtils.addDays(nextOpening, 1);
		}

		while (todaysOpening(nextOpening) == null) {
			nextOpening = CalendarUtils.addDays(nextOpening, 1);
		}

		return todaysOpening(nextOpening);
	}
	
	/**
	 * Returns the last closing time of the exchange before the given before(TimeStamp), taking into account holidays and business days defined for the exchange by this ExchangeCalendar.
	 */
	public TimeStamp getLastClosingAsTimeStamp(final TimeStamp before) {
		Check.notNull("before", before);

		TimeStamp lastClosing = before;
		if (todaysClosing(lastClosing) == null || before.isBefore(todaysClosing(lastClosing))) {
			/*
			 * We do not open today, or 'before' is not after the closing bell. Skip today...
			 */
			lastClosing = CalendarUtils.addDays(lastClosing, -1);
		}

		while (todaysClosing(lastClosing) == null) {
			lastClosing = CalendarUtils.addDays(lastClosing, -1);
		}

		return todaysClosing(lastClosing);
	}
	/**
	 * returns a TimeStamp with the actual opening time (in the associated timeZone(TimeZone)) for the given day(TimeStamp) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the opening time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 */
	public TimeStamp getOpeningTimeTodayAsTimeStamp(final TimeStamp day) {
		Check.notNull("day", day);
		return todaysOpening(day);
	}
	/**
	 * returns a TimeStamp with the actual closing time (in the associated timeZone(TimeZone)) for the given day(TimeStamp) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the closing time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 */
	public TimeStamp getCloseTimeTodayAsTimeStamp(final TimeStamp day) {
		Check.notNull("day", day);
		return todaysClosing(day);
	}
	/**
	 * returns the associated holidays(IHolidayCalendar)
	 */
	public IHolidayCalendar getHolidayCalendar() {
		return this.holidays;
	}
	/**
	 * returns whether the exchange will be open at the given when(TimeStamp) according to this ExchangeCalendar instance.
	 */
	public boolean isOpen(final TimeStamp when) {
		Check.notNull("when", when);

		TimeStamp todaysOpening = todaysOpening(when);
		TimeStamp todaysClosing = todaysClosing(when);

		if (todaysOpening == null || todaysClosing == null) {
			return false;
		}

		if (when.isBefore(todaysOpening) || when.isAfter(todaysClosing)) {
			return false;
		}

		return true;
	}
	/**
	 * Determines if a time stamp is during a holiday<br/>
	 * iterates the HolidayCalendarEntry elements in the associated holidays(IHolidayCalendar) and returns whether any of them
	 * is during the given when(TimeStamp)
	 */
	public boolean isHoliday(final TimeStamp when) {
		return this.holidays.isHoliday(when);
	}
	/**
	 * returns the associated timeZone(TimeZone)
	 */
	public TimeZone getTimeZone() {
		return this.timeZone;
	}
	/**
	 * returns the associated exchange(Exchange)
	 */
	public Exchange getExchange() {
		return this.exchange;
	}
	/**
	 * returns a <code>Map&lt;TimeStamp,Interval&lt;TimeStamp&gt;&gt;</code> with opening - closing times Intervals of this ExchangeCalendar for the given from(TimeStamp)-to(TimeStamp) time period, taking into
	 * account holidays and business days. The key of each entry of the map will be a <code>TimeStamp</code> for the day
	 * with the time part truncated to midnight. Only the days for which the exchange is at least partially opened will
	 * be returned as a entry in the map.
	 */
	public Map<TimeStamp,Interval<TimeStamp>> getTradingHours(TimeStamp from, TimeStamp to) {
		Check.isNotNullAndEqualOrAfter("from", "to", from, to);

		final Map<TimeStamp, Interval<TimeStamp>> map = new TreeMap<TimeStamp, Interval<TimeStamp>>();

		Calendar day = GregorianCalendar.getInstance(this.timeZone);
		day.setTimeInMillis(from.getDate().getTime());
		day = CalendarUtils.toMidnight(day);

		Calendar lastDay = GregorianCalendar.getInstance(this.timeZone);
		lastDay.setTimeInMillis(to.getDate().getTime());
//		lastDay = CalendarUtils.toMidnight(lastDay);

		while (day.before(lastDay)) {

			Calendar todaysOpening = this.todaysOpening(day);
			Calendar todaysClose = this.todaysClosing(day);

			if (todaysOpening != null && todaysClose != null) {
//				logger.info("getTradingHours(): " + CalendarUtils.calendarToString(day));
				map.put(new TimeStamp(day.getTimeInMillis() * CalendarUtils.MILLIS_TO_NANOS), new Interval<TimeStamp>(new TimeStamp(todaysOpening.getTimeInMillis() * CalendarUtils.MILLIS_TO_NANOS), new TimeStamp(todaysClose.getTimeInMillis() * CalendarUtils.MILLIS_TO_NANOS)));
			}

			day.add(Calendar.DAY_OF_MONTH, 1);
		}

		return map;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (int) (this.closingMillis ^ (this.closingMillis >>> 32));
		result = prime * result + ((this.entries == null) ? 0 : this.entries.hashCode());
		result = prime * result + ((this.exchange == null) ? 0 : this.exchange.hashCode());
		result = prime * result + ((this.holidays == null) ? 0 : this.holidays.hashCode());
		result = prime * result + (int) (this.openingMillis ^ (this.openingMillis >>> 32));
		result = prime * result + ((this.timeZone == null) ? 0 : this.timeZone.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ExchangeCalendar other = (ExchangeCalendar) obj;
		if (this.closingMillis != other.closingMillis)
			return false;
		if (this.entries == null) {
			if (other.entries != null)
				return false;
		} else if (!this.entries.equals(other.entries))
			return false;
		if (this.exchange == null) {
			if (other.exchange != null)
				return false;
		} else if (!this.exchange.equals(other.exchange))
			return false;
		if (this.holidays == null) {
			if (other.holidays != null)
				return false;
		} else if (!this.holidays.equals(other.holidays))
			return false;
		if (this.openingMillis != other.openingMillis)
			return false;
		if (this.timeZone == null) {
			if (other.timeZone != null)
				return false;
		} else if (!this.timeZone.equals(other.timeZone))
			return false;
		return true;
	}

	/*******************************************************************************************
	 * Private methods
	 *******************************************************************************************/
	/**
	 * returns a Calendar with the actual opening time for the given calendar(Calendar) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the opening time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 */
	private Calendar todaysOpening(final Calendar calendar) {
		Calendar opening = CalendarUtils.toMidnight((Calendar) calendar.clone());

		Calendar regularOpening = (Calendar) opening.clone();
		CalendarUtils.setTime(regularOpening, this.openingMillis);

		//		logger.info("opening: " + CalendarUtils.calendarToString(opening));
		//		logger.info("regularOpening: " + CalendarUtils.calendarToString(regularOpening));

		HolidayCalendarEntry holiday = this.holidays.getHoliday(opening);
		if (holiday != null) {

			if (holiday.isAllDayHoliday()) {
				return null;
			}

			if (holiday.getStartTimeInMillisAfterMidnight() > this.openingMillis) {
				/*
				 * Partial-day holiday starts after regular opening, so we open regular
				 */
				return regularOpening;
			} else {
				if (holiday.getEndTimeInMillisAfterMidnight() < this.closingMillis) {
					/*
					 * Partial-day holiday starts before regular opening, but ends before the regular closing, so we
					 * open after the partial-day holiday
					 */
					CalendarUtils.setTime(opening, holiday.getEndTimeInMillisAfterMidnight() + 1);
					return opening;
				} else {
					/*
					 * Partial-day holiday overlaps the regular opening and closing, so effectively we are closed the
					 * hole day
					 */
					return null;
				}
			}
		}

		if (!CalendarUtils.isBusinessDay(opening)) {
			return null;
		}

		return regularOpening;
	}
	/**
	 * returns a TimeStamp with the actual opening time (in the associated timeZone(TimeZone)) for the given day(TimeStamp) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the opening time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 * @param day
	 * @return
	 */
	private TimeStamp todaysOpening(final TimeStamp day) {
		Calendar calendar = GregorianCalendar.getInstance(this.timeZone);
		calendar.setTimeInMillis(day.getDate().getTime());

		Calendar todaysOpening = todaysOpening(calendar);
		if (todaysOpening == null) {
			return null;
		}
		return new TimeStamp(todaysOpening.getTimeInMillis() * CalendarUtils.MILLIS_TO_NANOS);
	}
	/**
	 * returns a Calendar with the actual closing time for the given calendar(Calendar) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the closing time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 * @param calendar
	 * @return
	 */
	private Calendar todaysClosing(final Calendar calendar) {

		Calendar close = CalendarUtils.toMidnight((Calendar) calendar.clone());

		Calendar regularClose = (Calendar) close.clone();
		CalendarUtils.setTime(regularClose, this.closingMillis);

		HolidayCalendarEntry holiday = this.holidays.getHoliday(close);
		if (holiday != null) {

			if (holiday.isAllDayHoliday()) {
				return null;
			}

			if (holiday.getEndTimeInMillisAfterMidnight() < this.closingMillis) {
				/*
				 * Partial-day holiday ends before regular close, so we close regular
				 */
				return regularClose;
			} else {
				if (holiday.getStartTimeInMillisAfterMidnight() > this.openingMillis) {
					/*
					 * Partial-day holiday starts after regular opening, and ends after the regular closing, so we close
					 * when the partial-day holiday starts
					 */
					CalendarUtils.setTime(close, holiday.getStartTimeInMillisAfterMidnight() - 1);
					return close;
				} else {
					/*
					 * Partial-day holiday overlaps the regular opening and closing, so effectively we are closed the
					 * hole day
					 */
					return null;
				}
			}
		}

		if (!CalendarUtils.isBusinessDay(calendar)) {
			return null;
		}

		return regularClose;
	}
	/**
	 * returns a TimeStamp with the actual closing time (in the associated timeZone(TimeZone)) for the given day(TimeStamp) according to this ExchangeCalendar:<br/>
	 * If the given calendar(Calendar) is in a non business day (Saturday or Sunday), or an all day holiday, or a partial holiday that overlaps the associated
	 * openingMillis(long) and closingMillis(long) times, it returns null (the exchange will not open that day).<br/>
	 * Otherwise it calculates and returns a Calendar with the closing time for that day by using the associated openingMillis(long) and closingMillis(long)
	 * and, if partial holiday, the startTimeInMillisAfterMidnight(Long) and endTimeInMillisAfterMidnight(Long) of that HolidayCalendarEntry.
	 * @param day
	 * @return
	 */
	private TimeStamp todaysClosing(final TimeStamp day) {
		Calendar calendar = GregorianCalendar.getInstance(this.timeZone);
		calendar.setTimeInMillis(day.getDate().getTime());

		Calendar todaysClosing = todaysClosing(calendar);
		if (todaysClosing == null) {
			return null;
		}
		return new TimeStamp(todaysClosing.getTimeInMillis() * CalendarUtils.MILLIS_TO_NANOS);
	}
}
